package pl.edu.agh.dsrg.sr.chat.communicator;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.jgroups.JChannel;
import org.jgroups.Message;
import org.jgroups.ReceiverAdapter;
import org.jgroups.protocols.BARRIER;
import org.jgroups.protocols.FD_ALL;
import org.jgroups.protocols.FD_SOCK;
import org.jgroups.protocols.FRAG2;
import org.jgroups.protocols.MERGE2;
import org.jgroups.protocols.MFC;
import org.jgroups.protocols.PING;
import org.jgroups.protocols.UDP;
import org.jgroups.protocols.UFC;
import org.jgroups.protocols.UNICAST2;
import org.jgroups.protocols.VERIFY_SUSPECT;
import org.jgroups.protocols.pbcast.FLUSH;
import org.jgroups.protocols.pbcast.GMS;
import org.jgroups.protocols.pbcast.NAKACK;
import org.jgroups.protocols.pbcast.STABLE;
import org.jgroups.protocols.pbcast.STATE_TRANSFER;
import org.jgroups.stack.ProtocolStack;

import pl.edu.agh.dsrg.sr.chat.common.ManagementChannelNotificationAppender;
import pl.edu.agh.dsrg.sr.chat.common.MessageChanelNotificationAppender;
import pl.edu.agh.dsrg.sr.chat.protos.ChatOperationProtos.ChatAction;
import pl.edu.agh.dsrg.sr.chat.protos.ChatOperationProtos.ChatAction.ActionType;
import pl.edu.agh.dsrg.sr.chat.protos.ChatOperationProtos.ChatMessage;

public class ChatCommunicator {

	private Map<String, List<String>> chatState;

	private String nick;
	private String managementChannelName;

	private ReceiverAdapter messageChannelReceiver;
	private ReceiverAdapter managementChannelReceiver;

	private JChannel managementChannel;
	private JChannel currentMessageChannel;
	private List<JChannel> messageChannels;

	private Object stateLock;

	public ChatCommunicator(String nick, String managementChanelName,
			ManagementChannelNotificationAppender managementAppender,
			MessageChanelNotificationAppender messageAppender) {
		super();
		this.nick = nick;
		this.managementChannelName = managementChanelName;

		this.messageChannelReceiver = new MessageChannelReceiver(
				messageAppender, this);
		this.managementChannelReceiver = new ManagementChannelReceiver(
				managementAppender, this);
	}

	public Object getStateLock() {
		return stateLock;
	}

	public Map<String, List<String>> getChatState() {
		return chatState;
	}

	public boolean setActiveSendingChannel(String channelName) {
		// TODO
		return true;
	}

	public String getActiveSendingChannelName() {
		return currentMessageChannel.getName();
	}

	public String getNick() {
		return nick;
	}

	public boolean leaveMessageChannel(String channelName) throws Exception {
		int index = getChannelIndexByName(channelName);
		if (index == -1) {
			System.err.println("NOT FOUND when leaving: " + channelName);
			return false;
		}

		JChannel leftChannel = messageChannels.remove(index);
		if (currentMessageChannel.getAddress().equals(channelName)) {
			if (messageChannels.size() > 1) {
				currentMessageChannel = messageChannels.get(0);
			} else {
				currentMessageChannel = null;
			}
		}

		leftChannel.disconnect();
		notifyManagementChannel(ActionType.LEAVE, channelName);
		return true;
	}

	private int getChannelIndexByName(String channelName) {
		int index = -1;
		JChannel channel;
		for (int i = 0; i < messageChannels.size(); i++) {
			channel = messageChannels.get(i);
			// System.out.println("SEARCHING: " + channel.getAddress());
			// System.out.println("SEARCHING: " + channel.getName());
			// System.out.println("SEARCHING: " + channel.getAddressAsString());
			// System.out.println("SEARCHING: " + channel.getClusterName());

			if (channel.getClusterName().equals(channelName)) {
				index = i;
				break;
			}
		}

		return index;
	}

	public void start() throws Exception {
		stateLock = new Object();
		messageChannels = new ArrayList<JChannel>();
		chatState = new HashMap<String, List<String>>();
		currentMessageChannel = null;

		managementChannel = new JChannel(false);

		ProtocolStack stack = new ProtocolStack();
		managementChannel.setProtocolStack(stack);
		stack.addProtocol(new UDP())
				.addProtocol(new PING())
				.addProtocol(new MERGE2())
				.addProtocol(new FD_SOCK())
				.addProtocol(
						new FD_ALL().setValue("timeout", 12000).setValue(
								"interval", 3000))
				.addProtocol(new VERIFY_SUSPECT()) //
				.addProtocol(new BARRIER()) //
				.addProtocol(new NAKACK()) //
				.addProtocol(new UNICAST2()) //
				.addProtocol(new STABLE()) //
				.addProtocol(new GMS()) //
				.addProtocol(new UFC()) //
				.addProtocol(new MFC()) //
				.addProtocol(new FRAG2()) //
				.addProtocol(new STATE_TRANSFER()) //
				.addProtocol(new FLUSH());
		stack.init();

		managementChannel.setName(nick);
		managementChannel.setReceiver(managementChannelReceiver);
		managementChannel.connect(managementChannelName);

		managementChannel.getState(null, 10000);
	}

	public void createChannel(String channelName) throws Exception {
		createOrJoin(channelName);
	}

	public void joinChannel(String channelName) throws Exception {
		createOrJoin(channelName);
	}

	private void createOrJoin(String channelName) throws UnknownHostException,
			Exception {
		JChannel newChannel = new JChannel(false);
		newChannel.setReceiver(messageChannelReceiver);
		initStackProtocols(newChannel, channelName);

		newChannel.setName(nick);
		newChannel.connect(channelName);
		messageChannels.add(newChannel);

		currentMessageChannel = newChannel;

		notifyManagementChannel(ActionType.JOIN, channelName);
	}

	public void sendMessage(String line) throws Exception {
		if (currentMessageChannel == null) {
			System.err.println("NOT ACTIVE CHANNEL TO SEND");
			return;
		}
		ChatMessage chatMessage = ChatMessage.newBuilder().setMessage(line)
				.build();

		byte[] buf = chatMessage.toByteArray();
		Message msg = new Message(null, null, buf);
		currentMessageChannel.send(msg);
	}

	public void stop() {
		for (JChannel channel : messageChannels) {
			channel.close();
		}

		managementChannel.close();
	}

	private void initStackProtocols(JChannel newJChannel, String channelName)
			throws UnknownHostException, Exception {
		ProtocolStack stack = new ProtocolStack();
		newJChannel.setProtocolStack(stack);
		stack.addProtocol(
				new UDP().setValue("mcast_group_addr",
						InetAddress.getByName(channelName)))
				// stack.addProtocol(new UDP())
				.addProtocol(new PING())
				.addProtocol(new MERGE2())
				.addProtocol(new FD_SOCK())
				.addProtocol(
						new FD_ALL().setValue("timeout", 12000).setValue(
								"interval", 3000))
				.addProtocol(new VERIFY_SUSPECT()) //
				.addProtocol(new BARRIER()) //
				.addProtocol(new NAKACK()) //
				.addProtocol(new UNICAST2()) //
				.addProtocol(new STABLE()) //
				.addProtocol(new GMS()) //
				.addProtocol(new UFC()) //
				.addProtocol(new MFC()) //
				.addProtocol(new FRAG2()) //
				.addProtocol(new STATE_TRANSFER()) //
				.addProtocol(new FLUSH());
		stack.init();
	}

	private void notifyManagementChannel(ActionType actionType,
			String channelName) throws Exception {

		ChatAction action = ChatAction.newBuilder().setAction(actionType)
				.setChannel(channelName).setNickname(nick).build();
		byte[] buf = action.toByteArray();
		Message msg = new Message(null, null, buf);
		managementChannel.send(msg);
	}

}
